// Copyright (C) 2009 Jesse Jones
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using MmmDoc;
using MmmDoc.Database;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace MmmDoc.Html
{
	internal static class WriteMarkupOverloads
	{
		#pragma warning disable 0169
		[WriteMarkup.Overload]
		private static void WriteMark(Writer writer, ParagraphMarkup markup, bool isCode, TypeDef declaringType)
		{
			writer.WriteElement("BR", null);
			if (!isCode)
				writer.WriteElement("BR", null);
			Array.ForEach(markup.Markup, n => WriteMarkup.Invoke(writer, n, isCode, declaringType));
		}
		
		[WriteMarkup.Overload]
		private static void WriteMark(Writer writer, TextMarkup markup, bool isCode, TypeDef declaringType)
		{
			string text = markup.Text;
			if (isCode)
			{
				text = DoFormatCode(text);
				writer.UnencodedWrite(text);
			}
			else
				writer.Write(text);
		}
		
		[WriteMarkup.Overload]
		private static void WriteMark(Writer writer, LinkMarkup markup, bool isCode, TypeDef declaringType)
		{
			writer.WriteElement("A", markup.Text, "href", markup.Url, "target", "_top");
		}
		
		[WriteMarkup.Overload]
		private static void WriteMark(Writer writer, BulletListMarkup markup, bool isCode, TypeDef declaringType)
		{
			DoWriteList(writer, "UL", markup.Markup, isCode, declaringType);
		}
		
		[WriteMarkup.Overload]
		private static void WriteMark(Writer writer, NumberListMarkup markup, bool isCode, TypeDef declaringType)
		{
			DoWriteList(writer, "OL", markup.Markup, isCode, declaringType);
		}
		
		[WriteMarkup.Overload]
		private static void WriteMark(Writer writer, SeeMarkup markup, bool isCode, TypeDef declaringType)
		{
			WriteCodeReference.Write(writer, markup.Target, markup.Text, declaringType);
		}
		
		[WriteMarkup.Overload]
		private static void WriteMark(Writer writer, NameReferenceMarkup markup, bool isCode, TypeDef declaringType)
		{
			writer.WriteElement("SPAN", markup.Name, "class", "name-ref");
		}
		
		[WriteMarkup.Overload]
		private static void WriteMark(Writer writer, CodePhraseMarkup markup, bool isCode, TypeDef declaringType)
		{
			writer.WriteElement("SPAN", markup.Text, "class", "inline-code");
		}
		
		[WriteMarkup.Overload]
		private static void WriteMark(Writer writer, CodeBlockMarkup markup, bool isCode, TypeDef declaringType)
		{
			writer.WriteStartTag("PRE", "class", "code");
			Array.ForEach(markup.Markup, n => WriteMarkup.Invoke(writer, n, true, declaringType));
			writer.WriteEndTag("PRE");
		}
		
		[WriteMarkup.Overload]
		private static void WriteMark(Writer writer, SequencedMarkup markup, bool isCode, TypeDef declaringType)
		{
			Array.ForEach(markup.Markup, n => WriteMarkup.Invoke(writer, n, isCode, declaringType));
		}
		
		[WriteCodeElement.Overload]
		private static void WriteElement(Writer writer, CodeElement element, string description, TypeDef declaringType)
		{
			writer.Write(element.FullName);
		}
		
		[WriteCodeElement.Overload]
		private static void WriteElement(Writer writer, TypeDef element, string description, TypeDef declaringType)
		{
			string fileName = element.FileName();
			string text = string.IsNullOrEmpty(description) ? element.FullName : description;
			
			if (declaringType != null)
			{
				writer.WriteElement("A", text, "href", fileName + ".html", "target", "_parent", "class", "type");
			}
			else
			{
				string link1 = string.Format("types/{0}-toc.html", fileName);
				string link2 = string.Format("top.contents.location='types/{0}-file.html';", fileName);
				
				writer.WriteElement("A", text, "target", "nav", "href", link1, "onClick", link2, "class", "member");
			}
		}
		
		[WriteCodeElement.Overload]
		private static void WriteElement(Writer writer, Member element, string description, TypeDef declaringType)
		{
			string name = element.DeclaringType.FileName();
			int i = Array.FindIndex(element.DeclaringType.Members, m => m.Key == element.Key);
			if (i >= 0)
			{
				if (declaringType != null && declaringType.FullName == element.DeclaringType.FullName)
				{
					writer.WriteElement("A", string.IsNullOrEmpty(description) ? element.Key : description, "href", name + "-file.html#M" + i, "target", "contents", "class", "member");
				}
				else
				{
					string link1 = name + "-toc.html";
					string link2 = string.Format("top.contents.location='{0}-file.html#M{1}';", name, i);
					
					string text = element.DeclaringType.FullName + "::" + element.Key;
					writer.WriteElement("A", string.IsNullOrEmpty(description) ? text : description, "target", "nav", "href", link1, "onClick", link2, "class", "member");
				}
			}
			else
				writer.Write(string.IsNullOrEmpty(description) ? element.FullName : description);
		}
		#pragma warning restore 0169
		
		#region Private Methods
		private static void DoWriteList(Writer writer, string tag, Markup[] markup, bool isCode, TypeDef declaringType)
		{
			writer.WriteStartTagLine(tag, "class", "list");
			foreach (Markup m in markup)
			{
				writer.WriteStartTagLine("LI", "class", "list");
					WriteMarkup.Invoke(writer, m, isCode, declaringType);
				writer.WriteEndTagLine("LI");
			}
			writer.WriteEndTagLine(tag);
		}
		
		private static string DoFormatCode(string text)
		{
			text = System.Web.HttpUtility.HtmlEncode(text);
			
			string endian = DoGetEndian(text);
			if (endian != null)
			{
				List<string> lines;
				if (endian == "\r\n")
					lines = new List<string>(text.Replace("\r\n", "\n").Split('\n'));
				else
					lines = new List<string>(text.Split(endian[0]));
				
				// It's common for the text within a <code> element to end with a 
				// new line but because we render it within a <PRE> element it shows
				// up as a blank line which looks goofy if the pre element has a background
				// element. So what we do here is remove one trailing blank line
				// (if users really want a trailing new line they can add two).
				if (lines.Count > 0 && lines[lines.Count - 1].All(c => char.IsWhiteSpace(c)))
					lines.RemoveAt(lines.Count - 1);
					
				// The xml will also indent all of the lines within the code element which we
				// need to get rid of so we can indent with the css file without worrying about
				// how much indentation the xml added.
				string expansion = Settings.ExpandTabs > 0 ? new string(' ', Settings.ExpandTabs) : null;
				for (int i = 0; i < lines.Count; ++i)
				{
					int count = 0;
					while (count < lines[i].Length && lines[i][count] == ' ')
						++count;
					
					lines[i] = lines[i].Substring(count);
					if (expansion != null)
						lines[i] = lines[i].Replace("\t", expansion);	
				}
				
				text = string.Join(endian, lines.ToArray());
			}
			
			text = DoStyleCode(text);
			
			return text;
		}
		
		private static string DoStyleCode(string text)
		{
			if (ms_codeRE == null)
				DoInitCodeRegex();
				
			MatchCollection matches = ms_codeRE.Matches(text);
			for (int i = matches.Count - 1; i >= 0; --i)
			{
				Match match = matches[i];
				
				GroupCollection groups = match.Groups;
				for (int j = 1; j <= groups.Count; ++j)
				{
					Group g = groups[j];
					if (g.Success)
					{
						string markup = string.Format("<SPAN class = \"{0}\">{1}</SPAN>",
							ms_codeClasses[j], text.Substring(g.Index, g.Length));
							
						text = text.Remove(g.Index, g.Length);
						text = text.Insert(g.Index, markup);
						break;
					}
				}
			}
			
			return text;
		}
		
		private static string DoGetEndian(string text)
		{
			for (int i = 0; i < text.Length; ++i)
			{
				if (text[i] == '\r')
				{
					if (i + 1 < text.Length && text[i + 1] == '\n')
						return "\n";
					else
						return "\r\r";
				}
				else if (text[i] == '\n')
					return "\n";
			}
			
			return null;
		}
		
		private static void DoInitCodeRegex()
		{
			var styles = new List<string>();
			var classes = new List<string>();
			classes.Add(string.Empty);					// note that we don't use the zeroeth code class index.
			
			classes.Add("comment");
			styles.Add(@"(?: // .* $) | (?: /\* (?: . | \r | \n)*? \*/)");
			
			classes.Add("keyword");
			styles.Add(@"\b(?: abstract | event | new | struct | as | explicit | null | switch | base | extern | object | this | bool | false | operator | throw | break | finally | out | true | byte | fixed | override | try | case | float | params | typeof | catch | for | private | uint | char | foreach | protected | ulong | checked | goto | public | unchecked | class | if | readonly | unsafe | const | implicit | ref | ushort | continue | in | return | using | decimal | int | sbyte | virtual | default | interface | sealed | volatile | delegate | internal | short | void | do | is | sizeof | while | double | lock | stackalloc | else | long | static | enum | namespace | string | by | on | ascending | descending | from | get | group | into | join | let | orderby | partial | partial | select | set | var | where | where | yield)\b");
			
			classes.Add("number");
			styles.Add(@"(?: (?<= \W) \d+ \. \d+ (?: [eE] [\+-]? \d+)? [FfDdMm]? (?= \W)) | (?: (?<= \W) \. \d+ (?: [eE] [\+-]? \d+)? [FfDdMm]? (?= \W)) | (?: (?<= \W) \d+ [eE] [\+-]? \d+ [FfDdMm]? (?= \W)) | (?: (?<= \W) \d+ [FfDdMm] (?= \W)) | (?: (?<= \W) \d+ (?= \W))");
			
			classes.Add("attribute");
			styles.Add(@"(?<= ^ \s* \[ (?: \w+ \s* :)? \s*) [\w\.]+ \s* (?= \]|\()");
			
			classes.Add("region");
			styles.Add(@"(?: \x23region .* $) | (?: \x23endregion)");
			
			classes.Add("preprocessor");
			styles.Add(@"(?: \x23if | \x23else | \x23elif | \x23endif | \x23define | \x23undef | \x23warning | \x23error | \x23line | (?: \x23pragma \s warning) | (?: \x23pragma \s checksum) | \x23pragma) .* $");
			
			classes.Add("string");
			styles.Add(@"(?: &quot; (?: [^\r\n\\] | \\.)*? &quot;) | (?: @&quot; (?: [^\\] | \\. | (?: &quot; &quot;))*? &quot;) | (?: &apos; (?: [^\r\n\\] | \\.)*? &apos;)");
			
			for (int i = 0; i < styles.Count; ++i)
				styles[i] = "(" + styles[i] + ")";
			string expr = string.Join(" | ", styles.ToArray());
			
			ms_codeRE = new Regex(expr, RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
			ms_codeClasses = classes.ToArray();
		}
		#endregion
		
		#region Fields
		private static Regex ms_codeRE;
		private static string[] ms_codeClasses;
		#endregion
	}
}
